package com.xinke.module.system.service.oauth2;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.xinke.component.common.enums.UserTypeEnum;
import com.xinke.component.common.exception.constant.GlobalErrorCodeConstants;
import com.xinke.component.common.util.date.DateUtils;
import com.xinke.component.common.util.sno.IdGen;
import com.xinke.component.security.security.core.LoginUser;
import com.xinke.component.tenant.core.context.TenantContextHolder;
import com.xinke.component.tenant.core.util.TenantUtils;
import com.xinke.module.system.mapper.oauth2.OAuth2RefreshTokenMapper;
import com.xinke.module.system.mapper.user.AdminUserMapper;
import com.xinke.module.system.model.entity.oauth2.OAuth2ClientDO;
import com.xinke.module.system.model.entity.oauth2.OAuth2RefreshTokenDO;
import com.xinke.module.system.model.entity.user.AdminUserDO;
import com.xinke.module.system.redis.oauth2.OAuth2AccessTokenRedisDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import com.xinke.module.system.model.vo.oauth2.*;
import com.xinke.module.system.model.entity.oauth2.OAuth2AccessTokenDO;
import com.xinke.component.common.pojo.PageResult;
import com.xinke.component.common.util.object.BeanUtils;

import com.xinke.module.system.mapper.oauth2.OAuth2AccessTokenMapper;

import static com.xinke.component.common.exception.util.ServiceExceptionUtil.exception;
import static com.xinke.component.common.exception.util.ServiceExceptionUtil.exception0;

/**
 * OAuth2 访问令牌 Service 实现类
 *
 * @author yzx
 */
@Service
@Validated
public class OAuth2AccessTokenServiceImpl implements OAuth2AccessTokenService {

    @Resource
    private OAuth2AccessTokenMapper oAuth2AccessTokenMapper;

    @Resource
    private AdminUserMapper userMapper;

    @Resource
    private OAuth2AccessTokenRedisDao accessTokenRedisDao;

    @Resource
    private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
    @Override
    public Long createOAuth2AccessToken(OAuth2AccessTokenSaveReqVO createReqVO) {
        // 插入
        OAuth2AccessTokenDO oAuth2AccessToken = BeanUtils.toBean(createReqVO, OAuth2AccessTokenDO.class);
        oAuth2AccessTokenMapper.insert(oAuth2AccessToken);
        // 返回
        return oAuth2AccessToken.getTokenId();
    }

    @Override
    public OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
        OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
                .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
                .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))
                .setClientNo(clientDO.getClientNo()).setAuthScope(refreshTokenDO.getAuthScope())
                .setRefreshToken(refreshTokenDO.getRefreshToken())
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
        accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号，避免缓存到 Redis 的时候，无对应的租户编号
        accessTokenDO.setTokenId(IdGen.nextId());
        oAuth2AccessTokenMapper.insert(accessTokenDO);
        // 记录到 Redis 中
        accessTokenRedisDao.set(accessTokenDO);
        return accessTokenDO;
    }

    @Override
    public void updateOAuth2AccessToken(OAuth2AccessTokenSaveReqVO updateReqVO) {
        // 校验存在
        validateOAuth2AccessTokenExists(updateReqVO.getTokenId());
        // 更新
        OAuth2AccessTokenDO updateObj = BeanUtils.toBean(updateReqVO, OAuth2AccessTokenDO.class);
        oAuth2AccessTokenMapper.updateById(updateObj);
    }

    @Override
    public void deleteOAuth2AccessToken(Long tokenId) {
        // 校验存在
        validateOAuth2AccessTokenExists(tokenId);
        // 删除
        oAuth2AccessTokenMapper.deleteById(tokenId);
    }

    private void validateOAuth2AccessTokenExists(Long tokenId) {
        if (oAuth2AccessTokenMapper.selectById(tokenId) == null) {
            throw exception(null);
        }
    }

    @Override
    public OAuth2AccessTokenDO getOAuth2AccessToken(Long tokenId) {
        return oAuth2AccessTokenMapper.selectById(tokenId);
    }

    @Override
    public PageResult<OAuth2AccessTokenDO> getOAuth2AccessTokenPage(OAuth2AccessTokenPageReqVO pageReqVO) {
        return oAuth2AccessTokenMapper.selectPage(pageReqVO);
    }

    @Override
    public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
        OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
        if (accessTokenDO == null) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在");
        }
        if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期");
        }
        return accessTokenDO;
    }
    @Override
    public OAuth2AccessTokenDO getAccessToken(String accessToken) {
        // 优先从 Redis 中获取
        OAuth2AccessTokenDO accessTokenDO = accessTokenRedisDao.get(accessToken);
        if (accessTokenDO != null) {
            return accessTokenDO;
        }

        // 获取不到，从 MySQL 中获取访问令牌
        accessTokenDO = oAuth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            // 特殊：从 MySQL 中获取刷新令牌。原因：解决部分场景不方便刷新访问令牌场景
            // 例如说，积木报表只允许传递 token，不允许传递 refresh_token，导致无法刷新访问令牌
            // 再例如说，前端 WebSocket 的 token 直接跟在 url 上，无法传递 refresh_token
            OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(accessToken);
            if (refreshTokenDO != null && !DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
                accessTokenDO = convertToAccessToken(refreshTokenDO);
            }
        }

        // 如果在 MySQL 存在，则往 Redis 中写入
        if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            accessTokenRedisDao.set(accessTokenDO);
        }
        return accessTokenDO;
    }

    private OAuth2AccessTokenDO convertToAccessToken(OAuth2RefreshTokenDO refreshTokenDO) {
        OAuth2AccessTokenDO accessTokenDO = BeanUtils.toBean(refreshTokenDO, OAuth2AccessTokenDO.class)
                .setAccessToken(refreshTokenDO.getRefreshToken());
        TenantUtils.execute(refreshTokenDO.getTenantId(),
                () -> accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())));
        return accessTokenDO;
    }
    /**
     * 加载用户信息，方便 {@link LoginUser} 获取到昵称、部门等信息
     *
     * @param userId 用户编号
     * @param userType 用户类型
     * @return 用户信息
     */
    private Map<String, String> buildUserInfo(Long userId, Integer userType) {
        if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
            AdminUserDO user = userMapper.selectById(userId);
            return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getUserNickName())
                    .put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build();
        } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
            // 注意：目前 Member 暂时不读取，可以按需实现
            return Collections.emptyMap();
        }
        return null;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
        // 删除访问令牌
        OAuth2AccessTokenDO accessTokenDO = oAuth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            return null;
        }
        oAuth2AccessTokenMapper.deleteById(accessTokenDO.getTokenId());
        accessTokenRedisDao.delete(accessToken);
        // 删除刷新令牌
        oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
        return accessTokenDO;
    }
    private static String generateAccessToken() {
        return IdUtil.fastSimpleUUID();
    }


}